# ======================================================================
# Copyright TOTAL / CERFACS / LIRMM (03/2020)
# Contributor: Adrien Suau (<adrien.suau@cerfacs.fr>
#                           <adrien.suau@lirmm.fr>)
#
# This software is governed by the CeCILL-B license under French law and
# abiding  by the  rules of  distribution of free software. You can use,
# modify  and/or  redistribute  the  software  under  the  terms  of the
# CeCILL-B license as circulated by CEA, CNRS and INRIA at the following
# URL "http://www.cecill.info".
#
# As a counterpart to the access to  the source code and rights to copy,
# modify and  redistribute granted  by the  license, users  are provided
# only with a limited warranty and  the software's author, the holder of
# the economic rights,  and the  successive licensors  have only limited
# liability.
#
# In this respect, the user's attention is drawn to the risks associated
# with loading,  using, modifying and/or  developing or reproducing  the
# software by the user in light of its specific status of free software,
# that  may mean  that it  is complicated  to manipulate,  and that also
# therefore  means that  it is reserved for  developers and  experienced
# professionals having in-depth  computer knowledge. Users are therefore
# encouraged  to load and  test  the software's  suitability as  regards
# their  requirements  in  conditions  enabling  the  security  of their
# systems  and/or  data to be  ensured and,  more generally,  to use and
# operate it in the same conditions as regards security.
#
# The fact that you  are presently reading this  means that you have had
# knowledge of the CeCILL-B license and that you accept its terms.
# ======================================================================
import numbers
import itertools

import numpy


class RangeLike:
    def __init__(self):
        pass

    def _get_iterable(self):
        raise NotImplementedError()

    def __iter__(self):
        yield from self._get_iterable()

    def __repr__(self) -> str:
        raise NotImplementedError()

    def __len__(self) -> int:
        raise NotImplementedError()

    def __reversed__(self):
        raise NotImplementedError()


def _repr_num(num) -> str:
    if isinstance(num, numbers.Integral):
        return str(num)
    else:
        return "{0:.3f}".format(num)


class PointRange(RangeLike):
    def __init__(self, point: float, repr_transformation=lambda x: x):
        super().__init__()
        self.point = point
        self._repr_trans = repr_transformation

    def _get_iterable(self):
        return [self.point]

    def __repr__(self) -> str:
        return _repr_num(self._repr_trans(self.point))

    def __len__(self) -> int:
        return 1

    def __reversed__(self):
        return self


class LinearRange(RangeLike):
    def __init__(self, start: float, stop: float, num: int):
        super().__init__()
        self.start = start
        self.stop = stop
        self.num = num

    def _get_iterable(self):
        return numpy.linspace(self.start, self.stop, self.num)

    def __repr__(self) -> str:
        return "{}_{}_{}".format(
            _repr_num(self.start), _repr_num(self.stop), _repr_num(self.num)
        )

    def __len__(self) -> numbers.Integral:
        return self.num

    def __reversed__(self):
        return LinearRange(self.stop, self.start, self.num)


class LinearIntRange(RangeLike):
    def __init__(
        self, start: int, stop: int, num: int = 50,
    ):
        super().__init__()
        self.start = start
        self.stop = stop
        self.num = num

    def _get_iterable(self):
        if self.start < self.stop:
            return numpy.linspace(self.start, self.stop, self.num).astype(numpy.int)
        else:
            return numpy.linspace(self.stop, self.start, self.num).astype(numpy.int)[
                ::-1
            ]

    def __repr__(self) -> str:
        return "{}_{}_{}".format(
            _repr_num(self.start), _repr_num(self.stop), _repr_num(self.num)
        )

    def __len__(self) -> int:
        return self.num

    def __reversed__(self):
        return LinearRange(self.stop, self.start, self.num)


class LogRange(RangeLike):
    def __init__(
        self, start: float, stop: float, num: int, base: float = 10,
    ):
        super().__init__()
        self.start = start
        self.stop = stop
        self.num = num
        self.base = base

    def _get_iterable(self):
        return numpy.logspace(self.start, self.stop, self.num, base=self.base)

    def __repr__(self) -> str:
        return "{}_{}_{}_{}".format(
            _repr_num(self.start),
            _repr_num(self.stop),
            _repr_num(self.num),
            _repr_num(self.base),
        )

    def __len__(self) -> int:
        return self.num

    def __reversed__(self):
        raise NotImplementedError()


class LogIntRange(RangeLike):
    def __init__(
        self, start: float, stop: float, num: int, base: float = 10,
    ):
        super().__init__()
        self.start = start
        self.stop = stop
        self.num = num
        self.base = base
        self._iterable = None

        for i in itertools.count():
            self._iterable = set(
                map(int, base ** numpy.linspace(self.start, self.stop, self.num + i))
            )
            if len(self._iterable) == num:
                break
            # Else loop until we have exactly "num" different values.
        self._iterable = sorted(self._iterable)

    def _get_iterable(self):
        yield from self._iterable

    def __repr__(self) -> str:
        return "{}_{}_{}_{}".format(
            _repr_num(self.start),
            _repr_num(self.stop),
            _repr_num(self.num),
            _repr_num(self.base),
        )

    def __len__(self) -> int:
        return self.num

    def __reversed__(self):
        raise NotImplementedError()
